JBoss Community Archive (Read Only)

RHQ

Multiple scripting languages in the CLI

The CLI has been refactored to support multiple languages and includes support for javascript and python as of RHQ 4.5.0.

Adding the support for a new scripting language has become an easy task that doesn't require the knowledge of many RHQ internals and so is ready for 3rd parties to get involved.

On this page I'll try to explain what's in it to make RHQ understand your language of choice.

For scripting RHQ uses the standard Java way of executing scripts in other languages - JSR 223, aka ScriptEngine. Therefore any language that an interpreter or compiler/executor written in Java and that has support for JSR 223 (or for which such support could be implemented) is a candidate for an RHQ scripting language. Languages with such support include javascript, python, ruby, groovy, clojure and I'm sure there are many others.

Once installed into RHQ, the scripting runtime is provided with the same "bindings" to RHQ regardless of the scripting language. All the same variables of the same types are available in each language.

ScriptEngineProvider

RHQ finds out about language modules using the Java services mechanism. You need to implement an org.rhq.scripting.ScriptEngineProvider interface and let Java know about your implementation by leaving the full class name of your implementation in the META-INF/services/org.rhq.scripting.ScriptEngineProvider file in the jar you're distributing.

Let's take a look at the implementation for python:

public class PythonScriptEngineProvider implements ScriptEngineProvider {

    @Override
    public String getSupportedLanguage() {
        return "python";
    }

    @Override
    public ScriptEngineInitializer getInitializer() {
        return new PythonScriptEngineInitializer();
    }

    @Override
    public CodeCompletion getCodeCompletion() {
        // XXX are we gonna support code completion for multiple langs in the
        // CLI?
        return null;
    }

}

That is rather simple, isn't it? images/author/images/icons/emoticons/wink.gif Granted, the provider doesn't support code completion for RHQ's interactive CLI, but hey... This is just a demo images/author/images/icons/emoticons/wink0.gif

You may have noticed the PythonScriptEngineInitializer class in there and you are right to think that that's where the meat of the implementation lies. Let's take a look at that class:

public class PythonScriptEngineInitializer implements ScriptEngineInitializer {

    private ScriptEngineManager engineManager = new ScriptEngineManager();

    @Override
    public ScriptEngine instantiate(Set<String> packages, PermissionCollection permissions) throws ScriptException {

        ScriptEngine eng = engineManager.getEngineByName("python");

        //XXX this is not working perfectly in jython
        //but we can't make it work perfectly either (from this side of the pond),
        //so let's just keep our fingers crossed..
        //http://www.jython.org/jythonbook/en/1.0/ModulesPackages.html#from-import-statements
        for (String pkg : packages) {
            eng.eval("from " + pkg + " import *\n");
        }

        //fingers crossed we can secure jython like this
        return permissions == null ? eng : new SandboxedScriptEngine(eng, permissions);
    }

    @Override
    public void installScriptSourceProvider(ScriptEngine scriptEngine, ScriptSourceProvider provider) {
        PySystemState sys = Py.getSystemState();
        if (sys != null) {
            sys.path_hooks.append(new PythonSourceProvider(provider));
        }
    }

    @Override
    public Set<String> generateIndirectionMethods(String boundObjectName, Set<Method> overloadedMethods) {
        if (overloadedMethods == null || overloadedMethods.isEmpty()) {
            return Collections.emptySet();
        }

        Set<Integer> argCnts = new HashSet<Integer>();
        for (Method m : overloadedMethods) {
            argCnts.add(m.getParameterTypes().length);
        }

        String methodName = overloadedMethods.iterator().next().getName();
        StringBuilder functionBody = new StringBuilder();

        functionBody.append("def ").append(methodName).append("(*args, **kwargs):\n");
        functionBody.append("\t").append("if len(kwargs) > 0:\n");
        functionBody.append("\t\t").append("raise ValueError(\"Named arguments not supported for Java methods\")\n");
        functionBody.append("\t").append("argCnt = len(args)\n");

        for (Integer argCnt : argCnts) {
            functionBody.append("\t").append("if argCnt == ").append(argCnt).append(":\n");
            functionBody.append("\t\treturn ").append(boundObjectName).append(".").append(methodName).append("(");
            int last = argCnt - 1;
            for (int i = 0; i < argCnt; ++i) {
                functionBody.append("args[").append(i).append("]");
                if (i < last) {
                    functionBody.append(", ");
                }
            }
            functionBody.append(")\n");
        }

        return Collections.singleton(functionBody.toString());
    }

    @Override
    public String extractUserFriendlyErrorMessage(ScriptException e) {
        return e.getMessage();
    }
}

That's a little bit more of a mouthful but still it's not too bad I think with cca 60 lines of code. Additional code of course exists to support source code providers (the PythonSourceProvider class), but let me just tell you that it, too, is not more than 100 lines (jython has quite nice APIs).

So what do all those methods do?

  • instantiate() does just that - it instantiates a new script engine and initializes it. It tries to pre-import the supplied java packages (this is so that certain RHQ classes are available by default) and optionally secures the script engine using the given set of permissions that the scripts should run with (the SandboxedScriptEngine class is a ScriptEngine decorator provided by RHQ that runs all the eval methods with the specified java security permissions).

  • installScriptSourceProvider() enhances the script engine so that it is able to locate scripts with assistance of the provided ScriptSourceProvider instance. Usually this requires deep interaction with the underlying script engine implementation because Java's APIs don't support such functionality.

  • generateIndirectionMethods returns a set of function source codes that will make functions on a certain object accessible as "top level" methods. This is to support RHQ's standard variables and methods that are made available to the scripts.

  • extractUserFriendlyErrorMessage is there to extract an error message out of an exception thrown by the script engine during a script evaluation. This method exists because some script engines (like Rhino that RHQ uses for javascript support) have the habit of wrapping the exceptions thrown during script evaluation in a rather weird way.

You can see that with less than a 200 lines of code in 3 files I was able to implement a usable support for python in RHQ's CLI. The only thing missing is support for code completion in RHQ's interactive CLI console (but that's actually something I don't find particularly important, even though it would good to have).

JBoss.org Content Archive (Read Only), exported from JBoss Community Documentation Editor at 2020-03-11 12:43:29 UTC, last content change 2012-08-27 17:07:56 UTC.